{{#platforms}}
{{#node}}
// TODO: evaluate if we can easily get rid of this library
import {{^supportsES6}}* as{{/supportsES6}} FormData from "form-data";
import { URL, URLSearchParams } from 'url';
import { type Dispatcher } from 'undici';
{{/node}}
{{/platforms}}
import { Observable, from } from {{#useRxJS}}'rxjs'{{/useRxJS}}{{^useRxJS}}'../rxjsStub{{importFileExtension}}'{{/useRxJS}};

{{#platforms}}
{{^deno}}
{{#frameworks}}
{{#fetch-api}}
export * from './isomorphic-fetch{{importFileExtension}}';
{{/fetch-api}}
{{#jquery}}
export * from './jquery{{importFileExtension}}';
{{/jquery}}
{{/frameworks}}
{{/deno}}
{{/platforms}}

/**
 * Represents an HTTP method.
 */
{{#useErasableSyntax}}
export const HttpMethod = {
    GET: "GET",
    HEAD: "HEAD",
    POST: "POST",
    PUT: "PUT",
    DELETE: "DELETE",
    CONNECT: "CONNECT",
    OPTIONS: "OPTIONS",
    TRACE: "TRACE",
    PATCH: "PATCH"
} as const;

export type HttpMethod = typeof HttpMethod[keyof typeof HttpMethod];
{{/useErasableSyntax}}
{{^useErasableSyntax}}
export enum HttpMethod {
    GET = "GET",
    HEAD = "HEAD",
    POST = "POST",
    PUT = "PUT",
    DELETE = "DELETE",
    CONNECT = "CONNECT",
    OPTIONS = "OPTIONS",
    TRACE = "TRACE",
    PATCH = "PATCH"
}
{{/useErasableSyntax}}

/**
 * Represents an HTTP file which will be transferred from or to a server.
 */
{{#platforms}}
{{#node}}
export type HttpFile = {
    data: {{{fileContentDataType}}},
    name: string
};
{{/node}}
{{^node}}
export type HttpFile = {{{fileContentDataType}}} & { readonly name: string };
{{/node}}
{{/platforms}}

export class HttpException extends Error {
    public constructor(msg: string) {
        super(msg);
    }
}

/**
 * Represents the body of an outgoing HTTP request.
 */
export type RequestBody = undefined | string | FormData | URLSearchParams;

type Headers = Record<string, string>;

function ensureAbsoluteUrl(url: string) {
    if (url.startsWith("http://") || url.startsWith("https://")) {
        return url;
    }
    {{#platforms}}
    {{#node}}
    throw new Error("You need to define an absolute base url for the server.");
    {{/node}}
    {{^node}}
    return window.location.origin + url;
    {{/node}}
    {{/platforms}}
}

/**
 * Represents an HTTP request context
 */
export class RequestContext {
    private headers: Headers = {};
    private body: RequestBody = undefined;
    private url: URL;
    private signal: AbortSignal | undefined = undefined;
    {{#platforms}}
    {{#node}}
    private dispatcher: Dispatcher | undefined = undefined;
    {{/node}}
    {{/platforms}}

    /**
     * Creates the request context using a http method and request resource url
     *
     * @param url url of the requested resource
     * @param httpMethod http method
     */
    public constructor(url: string, private httpMethod: HttpMethod) {
        this.url = new URL(ensureAbsoluteUrl(url));
    }

    /*
     * Returns the url set in the constructor including the query string
     *
     */
    public getUrl(): string {
        return this.url.toString().endsWith("/") ?
            this.url.toString().slice(0, -1)
            : this.url.toString();
    }

    /**
     * Replaces the url set in the constructor with this url.
     *
     */
    public setUrl(url: string) {
        this.url = new URL(ensureAbsoluteUrl(url));
    }

    /**
     * Sets the body of the http request either as a string or FormData
     *
     * Note that setting a body on a HTTP GET, HEAD, DELETE, CONNECT or TRACE
     * request is discouraged.
     * https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#rfc.section.7.3.1
     *
     * @param body the body of the request
     */
    public setBody(body: RequestBody) {
        this.body = body;
    }

    public getHttpMethod(): HttpMethod {
        return this.httpMethod;
    }

    public getHeaders(): Headers {
        return this.headers;
    }

    public getBody(): RequestBody {
        return this.body;
    }

    public setQueryParam(name: string, value: string) {
        this.url.searchParams.set(name, value);
    }

    public appendQueryParam(name: string, value: string) {
        this.url.searchParams.append(name, value);
    }

    /**
     * Sets a cookie with the name and value. NO check  for duplicate cookies is performed
     *
     */
    public addCookie(name: string, value: string): void {
        if (!this.headers["Cookie"]) {
            this.headers["Cookie"] = "";
        }
        this.headers["Cookie"] += name + "=" + value + "; ";
    }

    public setHeaderParam(key: string, value: string): void  {
        // Delete any existing headers with case-insensitive matching keys
        const lowerKey = key.toLowerCase();
        for (const existingKey in this.headers) {
            if (existingKey.toLowerCase() === lowerKey) {
                delete this.headers[existingKey];
            }
        }
        this.headers[key] = value;
    }

    public setSignal(signal: AbortSignal): void {
        this.signal = signal;
    }

    public getSignal(): AbortSignal | undefined {
        return this.signal;
    }

    {{#platforms}}
    {{#node}}
    public setDispatcher(dispatcher: Dispatcher): void {
        this.dispatcher = dispatcher;
    }

    public getDispatcher(): Dispatcher | undefined {
        return this.dispatcher;
    }
    {{/node}}
    {{/platforms}}
}

export interface ResponseBody {
    text(): Promise<string>;
    binary(): Promise<{{{fileContentDataType}}}>;
}

/**
 * Helper class to generate a `ResponseBody` from binary data
 */
export class SelfDecodingBody implements ResponseBody {
    constructor(private dataSource: Promise<{{{fileContentDataType}}}>) {}

    binary(): Promise<{{{fileContentDataType}}}> {
        return this.dataSource;
    }

    async text(): Promise<string> {
        const data: {{{fileContentDataType}}} = await this.dataSource;
        {{#platforms}}
        {{#node}}
        return data.toString();
        {{/node}}
        {{#browser}}
        // @ts-ignore
        if (data.text) {
            // @ts-ignore
            return data.text();
        }

        return new Promise<string>((resolve, reject) => {
            const reader = new FileReader();
            reader.addEventListener("load", () => resolve(reader.result as string));
            reader.addEventListener("error", () => reject(reader.error));
            reader.readAsText(data);
        });
        {{/browser}}
        {{#deno}}
        return data.text();
        {{/deno}}
        {{/platforms}}
    }
}

export class ResponseContext {
    public constructor(
        public httpStatusCode: number,
        public headers: Headers,
        public body: ResponseBody
    ) {}

    /**
     * Parse header value in the form `value; param1="value1"`
     *
     * E.g. for Content-Type or Content-Disposition
     * Parameter names are converted to lower case
     * The first parameter is returned with the key `""`
     */
    public getParsedHeader(headerName: string): Headers {
        const result: Headers = {};
        if (!this.headers[headerName]) {
            return result;
        }

        const parameters = this.headers[headerName]!.split(";");
        for (const parameter of parameters) {
            let [key, value] = parameter.split("=", 2);
            if (!key) {
                continue;
            }
            key = key.toLowerCase().trim();
            if (value === undefined) {
                result[""] = key;
            } else {
                value = value.trim();
                if (value.startsWith('"') && value.endsWith('"')) {
                    value = value.substring(1, value.length - 1);
                }
                result[key] = value;
            }
        }
        return result;
    }

    public async getBodyAsFile(): Promise<HttpFile> {
        const data = await this.body.binary();
        const fileName = this.getParsedHeader("content-disposition")["filename"] || "";
        {{#platforms}}
        {{#node}}
        return { data, name: fileName };
        {{/node}}
        {{^node}}
        const contentType = this.headers["content-type"] || "";
        try {
            return new File([data], fileName, { type: contentType });
        } catch (error) {
            /** Fallback for when the File constructor is not available */
            return Object.assign(data, {
                name: fileName,
                type: contentType
            });
        }
        {{/node}}
        {{/platforms}}
    }

    /**
     * Use a heuristic to get a body of unknown data structure.
     * Return as string if possible, otherwise as binary.
     */
    public getBodyAsAny(): Promise<string | {{{fileContentDataType}}} | undefined> {
        try {
            return this.body.text();
        } catch {}

        try {
            return this.body.binary();
        } catch {}

        return Promise.resolve(undefined);
    }
}

export interface HttpLibrary {
    send(request: RequestContext): Observable<ResponseContext>;
}

export interface PromiseHttpLibrary {
    send(request: RequestContext): Promise<ResponseContext>;
}

export function wrapHttpLibrary(promiseHttpLibrary: PromiseHttpLibrary): HttpLibrary {
  return {
    send(request: RequestContext): Observable<ResponseContext> {
      return from(promiseHttpLibrary.send(request));
    }
  }
}

export class HttpInfo<T> extends ResponseContext {
    public constructor(
        httpStatusCode: number,
        headers: Headers,
        body: ResponseBody,
        public data: T,
    ) {
        super(httpStatusCode, headers, body);
    }
}
